#include <GL/glew.h>

#include <LightImage.h>

#include "common.h"
#include "drawing.h"
#include "string.h"
#include "stdio.h"
#include "math.h"
#include "log.h"
#include "input.h"

namespace Tmpl {

static int sDepthStack[DEPTHSTK_STACKSIZE] = {0};
static int sDepthStackIdx = 0, sDrawDepth = 0;

static inline void SetGlColor(Pixel color)
{
	glColor4ub((color&RMASK)>>RSHIFT, (color&GMASK)>>GSHIFT, (color&BMASK)>>BSHIFT, (color&AMASK)>>ASHIFT);
}

void Draw::Clear()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

// ===========================================================
// Blending functions
// ===========================================================

static Draw::BlendMode sCurrentBlendMode = Draw::NORMAL;
static bool sBlendingOn = false, sLogicOpOn = false;

void Draw::SetBlendMode(Draw::BlendMode newMode)
{
	if(sCurrentBlendMode != newMode)
	{
		bool enableBlending = false, enableLogicOp = false;

		// Set correct blending function
		switch(newMode)
		{
		case Draw::ALPHA:
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 
			enableBlending = true;
			enableLogicOp = false;
			break;

		case Draw::ADD:
			glBlendFunc(GL_SRC_ALPHA, GL_ONE);
			enableBlending = true;
			enableLogicOp = false;
			break;

		case Draw::INVERT:
			glLogicOp(GL_INVERT);
			enableBlending = false;
			enableLogicOp = true;
			break;

		case Draw::NORMAL:
			enableBlending = false;
			enableLogicOp = false;
		}
		
		// Enable or disable blending
		if(sBlendingOn && !enableBlending) glDisable(GL_BLEND);
		if(enableBlending && !sBlendingOn) glEnable(GL_BLEND);
		sBlendingOn = enableBlending;
		
		// Enable or disable color op
		if(sLogicOpOn && !enableLogicOp) glDisable(GL_COLOR_LOGIC_OP);
		if(enableLogicOp && !sLogicOpOn) glEnable(GL_COLOR_LOGIC_OP);
		sLogicOpOn = enableLogicOp;

		sCurrentBlendMode = newMode;
	}
}

// ===========================================================
// Depth functions
// ===========================================================

void Draw::DepthPush(int depth)
{
	const int i = ++sDepthStackIdx;
	if(i > 0 && i < DEPTHSTK_STACKSIZE) sDepthStack[i] = depth;
	if(i >= 0 && i < DEPTHSTK_STACKSIZE) sDrawDepth = sDepthStack[sDepthStackIdx];
}

void Draw::DepthPop()
{
	const int i = --sDepthStackIdx;
	if(i >= 0 && i < DEPTHSTK_STACKSIZE) sDrawDepth = sDepthStack[sDepthStackIdx];
}

int Draw::GetDepth()
{
	return sDrawDepth;
}

// ===========================================================
// ClipRect functions
// ===========================================================

static struct ClipRect
{
	ClipRect() : stackIdx(0), windowH(WINDOW_H) {} 
	int stackIdx, windowH;
	int stackX1[CLIPRECT_STACKSIZE];
	int stackY1[CLIPRECT_STACKSIZE];
	int stackX2[CLIPRECT_STACKSIZE];
	int stackY2[CLIPRECT_STACKSIZE];
}
clipRect;

void Draw::ClipRectAdjust(unsigned int windowWidth, unsigned int windowHeight)
{
	clipRect.stackX2[0] = windowWidth;
	clipRect.stackY2[0] = windowHeight;
	glScissor(0, 0, windowWidth, windowHeight);
	clipRect.windowH = windowHeight;
}

void Draw::ClipRectPush(int x1, int y1, int x2, int y2)
{
	const int i = ++clipRect.stackIdx;
	if(i >= 0 && i < CLIPRECT_STACKSIZE)
	{
		if(i > 0)
		{
			x2 = Max(x1, x2), y2 = Max(y1, y2);
			clipRect.stackX1[i] = Max(clipRect.stackX1[i-1], x1);
			clipRect.stackY1[i] = Max(clipRect.stackY1[i-1], y1);
			clipRect.stackX2[i] = Min(clipRect.stackX2[i-1], x2);
			clipRect.stackY2[i] = Min(clipRect.stackY2[i-1], y2);
		}
		const int w = clipRect.stackX2[i] - clipRect.stackX1[i];
		const int h = clipRect.stackY2[i] - clipRect.stackY1[i];
		glScissor(clipRect.stackX1[i], clipRect.windowH - clipRect.stackY2[i], w, h);
	}
}

void Draw::ClipRectPop()
{
	const int i = --clipRect.stackIdx;
	if(i >= 0 && i < CLIPRECT_STACKSIZE)
	{
		const int w = clipRect.stackX2[i] - clipRect.stackX1[i];
		const int h = clipRect.stackY2[i] - clipRect.stackY1[i];
		glScissor(clipRect.stackX1[i], clipRect.windowH - clipRect.stackY2[i], w, h);
	}
}

// ===========================================================
// Line drawing functions
// ===========================================================

void Draw::LineH(int x, int y, int x2, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_LINES);
		glVertex3i(x, y, sDrawDepth);
		glVertex3i(x2, y, sDrawDepth);
	glEnd();
}

void Draw::LineH( int x, int y, int x2, Pixel color1, Pixel color2)
{
	glBegin(GL_LINES);
		SetGlColor(color1); glVertex3i(x, y, sDrawDepth);
		SetGlColor(color2); glVertex3i(x2, y, sDrawDepth);
	glEnd();
}

void Draw::LineV( int x, int y, int y2, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_LINES);
		glVertex3i(x, y, sDrawDepth);
		glVertex3i(x, y2, sDrawDepth);
	glEnd();
}

void Draw::LineV( int x, int y, int y2, Pixel color1, Pixel color2 )
{
	glBegin(GL_LINES);
		SetGlColor(color1); glVertex3i(x, y, sDrawDepth);
		SetGlColor(color2); glVertex3i(x, y2, sDrawDepth);
	glEnd();
}

void Draw::Line(int x1, int y1, int x2, int y2, Pixel color)
{ 
	SetGlColor(color);
	glBegin(GL_LINES);
		glVertex3i(x1, y1, sDrawDepth);
		glVertex3i(x2, y2, sDrawDepth);
	glEnd();
}

void Draw::Line(float x1, float y1, float x2, float y2, Pixel color)
{ 
	SetGlColor(color);
	glBegin(GL_LINES);
		glVertex3f(x1, y1, (float)sDrawDepth);
		glVertex3f(x2, y2, (float)sDrawDepth);
	glEnd();
}

void Draw::Line(int x1, int y1, int x2, int y2, Pixel color1, Pixel color2)
{ 
	glBegin(GL_LINES);
		SetGlColor(color1); glVertex3i(x1, y1, sDrawDepth);
		SetGlColor(color2); glVertex3i(x2, y2, sDrawDepth);
	glEnd(); 
}

void Draw::Line(float x1, float y1, float x2, float y2, Pixel color1, Pixel color2)
{ 
	glBegin(GL_LINES);
		SetGlColor(color1); glVertex3f(x1, y1, (float)sDrawDepth);
		SetGlColor(color2); glVertex3f(x2, y2, (float)sDrawDepth);
	glEnd(); 
}

// ===========================================================
// Geometrical drawing functions
// ===========================================================

void Draw::Point(int x, int y, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_POINTS);
		glVertex3i(x, y, sDrawDepth);
	glEnd();
}

void Draw::Box(int x1, int y1, int x2, int y2, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_LINE_LOOP);
		glVertex3i(x1+0, y2-1, sDrawDepth); // without this vertex the bottom left corner is not filled
		glVertex3i(x1+1, y2-1, sDrawDepth);
		glVertex3i(x1+1, y1+0, sDrawDepth);
		glVertex3i(x2+0, y1+0, sDrawDepth);
		glVertex3i(x2+0, y2-1, sDrawDepth);
	glEnd();
}

void Draw::Bar(int x1, int y1, int x2, int y2, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_QUADS);
		glVertex3i(x1, y1, sDrawDepth);
		glVertex3i(x2, y1, sDrawDepth);
		glVertex3i(x2, y2, sDrawDepth);
		glVertex3i(x1, y2, sDrawDepth);
	glEnd();
}

void Draw::BarV(int x1, int y1, int x2, int y2, Pixel colorTop, Pixel colorBtm)
{
	glBegin(GL_QUADS);
		SetGlColor(colorTop);
		glVertex3i(x1, y1, sDrawDepth);
		glVertex3i(x2, y1, sDrawDepth);
		SetGlColor(colorBtm); 
		glVertex3i(x2, y2, sDrawDepth);	
		glVertex3i(x1, y2, sDrawDepth);
	glEnd();
}

void Draw::BarH(int x1, int y1, int x2, int y2, Pixel colorLeft, Pixel colorRight)
{
	glBegin(GL_QUADS);
		SetGlColor(colorLeft); 
		glVertex3i(x1, y2, sDrawDepth);
		glVertex3i(x1, y1, sDrawDepth);
		SetGlColor(colorRight); 
		glVertex3i(x2, y1, sDrawDepth);
		glVertex3i(x2, y2, sDrawDepth);
	glEnd();
}

void Draw::Circle(int x, int y, float radius, int segments, bool fill, Pixel color)
{
	const float flX = (float)x, flY = (float)y, depth = (float)sDrawDepth;
	SetGlColor(color);
	glBegin(fill ? GL_POLYGON : GL_LINE_LOOP);
	const float delta = 2*PI / (float)(segments + 1);
	for(float i=0; i <= 2*PI; i += delta)
	{
		float circleX = flX + radius * cosf(i);
		float circleY = flY + radius * sinf(i);
		glVertex3f(circleX, circleY, depth);
	}
	glEnd();
}

static void CatmulSegment(const Vec2f& p0, const Vec2f& p1, 
						  const Vec2f& p2, const Vec2f& p3, 
						  float delta, float depth)
{
	glVertex3f(p1.x, p1.y, depth);
	for(float t=delta; t<1.0f; t+=delta)
	{
		const float t2 = t * t;
		const float t3 = t2 * t;
		const Vec2f thisP = 0.5f *
			((2.0f*p1) +
			(-p0 + p2) * t +
			(2.0f*p0 - 5.0f*p1 + 4.0f*p2 - p3) * t2 +
			(-p0 + 3.0f*p1 - 3.0f*p2 + p3) * t3);

		glVertex3f(thisP.x, thisP.y, depth);	
	}
}

void Draw::CatmullRom(const Vec2f* points, unsigned int pointCount, unsigned int subSegments, Pixel color)
{
	SetGlColor(color);
	const float fDelta = 1.0f / (float)(subSegments + 1);
	const float depth = (float)sDrawDepth;
	
	// Special cases with too few points
	switch(pointCount)
	{
	case 0: return;
	case 1:
		glBegin(GL_POINTS);
			glVertex3f(points[0].x, points[0].y, depth);
		glEnd();
		return;
	case 2:
		glBegin(GL_LINES);
			glVertex3f(points[0].x, points[0].y, depth);
			glVertex3f(points[1].x, points[1].y, depth);
		glEnd();
		return;
	case 3:
		glBegin(GL_LINE_STRIP);
			CatmulSegment(points[0], points[0], points[1], points[2], fDelta, depth);
			CatmulSegment(points[0], points[1], points[2], points[2], fDelta, depth);
			glVertex3f(points[2].x, points[2].y, depth);
		glEnd();
	default:
		break;
	};

	// Enough segments to draw a full catmul rom
	glBegin(GL_LINE_STRIP);
	CatmulSegment(points[0], points[0], points[1], points[2], fDelta, depth);
	for(unsigned int i=0; i<pointCount-3; ++i)
	{
		CatmulSegment(points[i], points[i+1], points[i+2], points[i+3], fDelta, depth);
	}
	CatmulSegment(points[pointCount-3], points[pointCount-2], 
				  points[pointCount-1], points[pointCount-1], fDelta, depth);
	glEnd();
}

// ===========================================================
// General Texture functions
// ===========================================================

static void SendQuadVertices(int x, int y, int w, int h, const ImageUvRect& uvs)
{
	glTexCoord2d(uvs.u0, uvs.v0);	glVertex3i(x+0, y+0, sDrawDepth);
	glTexCoord2d(uvs.u1, uvs.v0);	glVertex3i(x+w, y+0, sDrawDepth);
	glTexCoord2d(uvs.u1, uvs.v1);	glVertex3i(x+w, y+h, sDrawDepth);
	glTexCoord2d(uvs.u0, uvs.v1);	glVertex3i(x+0, y+h, sDrawDepth);
}

static void SendQuadVertices(const Vec2f& tl, const Vec2f& tr, const Vec2f& bl, const Vec2f& br, const ImageUvRect& uvs)
{
	const float drawDepth = (float)sDrawDepth;
	glTexCoord2d(uvs.u0, uvs.v0);	glVertex3f(tl.x, tl.y, drawDepth);
	glTexCoord2d(uvs.u1, uvs.v0);	glVertex3f(tr.x, tr.y, drawDepth);
	glTexCoord2d(uvs.u1, uvs.v1);	glVertex3f(br.x, br.y, drawDepth);
	glTexCoord2d(uvs.u0, uvs.v1);	glVertex3f(bl.x, bl.y, drawDepth);
}

static void RenderTexQuad(int x, int y, int w, int h, const ImageUvRect& uvs, Pixel color)
{
	SetGlColor(color);
	glBegin(GL_QUADS);
		SendQuadVertices(x, y, w, h, uvs);
	glEnd();
}

static void RenderTexQuad(float x, float y, float w, float h, float rotation, const Vec2f& origin, const ImageUvRect& uvs, Pixel color)
{
	const float ox = -origin.x, sx = ox + w;
	const float oy = -origin.y, sy = oy + h;

	Matrix3x3f mat(Vec2f(x,y), rotation);
	const Vec2f tl = mat.Transform(Vec2f(ox, oy));
	const Vec2f tr = mat.Transform(Vec2f(sx, oy));
	const Vec2f bl = mat.Transform(Vec2f(ox, sy));
	const Vec2f br = mat.Transform(Vec2f(sx, sy));

	SetGlColor(color);
	glBegin(GL_QUADS);
		SendQuadVertices(tl, tr, bl, br, uvs);
	glEnd();
}


// ===========================================================
// Image class
// ===========================================================

bool Image::mForcePowerOfTwo = false;

void Image::DestroyPixels()
{
	if(mBuffer && !myHasExternalBuffer) FREE64(mBuffer);
	mBuffer = 0;
	myHasExternalBuffer = false;
}

void Image::DestroyTexture()
{
	if(mOpenGlTexId && !myHasExternalTexId) glDeleteTextures(1, (GLuint*)&mOpenGlTexId);
	mOpenGlTexId = 0;
	myHasExternalTexId = false;
	mUVs.u0 = mUVs.v0 = 0.0f;
	mUVs.u1 = mUVs.v1 = 1.0f;
}

void Image::ForcePowerOfTwoTextures(bool enable)
{
	mForcePowerOfTwo = enable;
}

bool Image::Create(const char* imageFile)
{
	DestroyPixels();
	DestroyTexture();

	// Load image from file
	FILE* fp;
	fopen_s(&fp, imageFile, "rb");
	if(!fp) return false;

	int w, h, format;
	Pixel* bits = (Pixel*)stbi_load_from_file(fp, &w, &h, &format, STBI_rgb_alpha);
	fclose(fp);

	if(!bits)
	{
		const char* reason = stbi_failure_reason();
		LOG_ERROR("Unable to load Image \"%s\"", imageFile);
		return false;
	}
	mSize = Vec2i(w, h);

	// Copy image bits
	mBuffer = MALLOC64(mSize.x * mSize.y, Pixel);
	for(int y=0; y<mSize.y; ++y)
	{
		const Pixel* line = bits + y * mSize.x;
		Pixel* dst = mBuffer + y * mSize.x;
		for(int x=0; x<mSize.x; ++x) dst[x] = line[x];
	}
	stbi_image_free(bits);
	return true;
}

void Image::Create(Pixel* bufferRGBA, int w, int h, bool copyPixels)
{	
	DestroyPixels();
	DestroyTexture();

	mSize = Vec2i(w, h);
	if(copyPixels)
	{
		mBuffer = MALLOC64(mSize.x * mSize.y, Pixel);
		for(int y=0; y<h; ++y)
		{
			Pixel* dst = mBuffer + y*mSize.x;
			const Pixel* src = bufferRGBA + y*w;
			for(int x=0; x<w; ++x) dst[x] = src[x];
		}
	}
	else
	{
		mBuffer = bufferRGBA;
		myHasExternalBuffer = true;
	}
}

bool Image::BuildTex(int texFlags, bool destroyPixels)
{
	if(!HasBuffer()) return false;

	DestroyTexture();

	glEnable(GL_TEXTURE_2D);
	glGenTextures(1, (GLuint*)&mOpenGlTexId);
	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);

	// Calculate texture dimensions
	int texW = mSize.x, texH = mSize.y;
	mUVs = ImageUvRect();
	if(mForcePowerOfTwo)
	{
		texW = 1; while(texW < mSize.x) texW *= 2;
		texH = 1; while(texH < mSize.y) texH *= 2;

		mUVs.u1 = (double)mSize.x / (double)texW;
		mUVs.v1 = (double)mSize.y / (double)texH;
	}

	// Set OpenGl texture parameters
	GLint wrapST = (texFlags & TEX_CLAMP) ? GL_CLAMP : GL_REPEAT;
	GLint magFilter = (texFlags & TEX_FILTER) ? GL_LINEAR : GL_NEAREST, minFilter = magFilter;
	if(texFlags & TEX_MIPMAP)
	{
		minFilter = (texFlags & TEX_FILTER) ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR;
	}

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
	
	// Assign buffer to texture
	const int bufW = mSize.x, bufH = mSize.y;
	if(texFlags & TEX_MIPMAP)
	{
		Pixel* blackBuffer = MALLOC64(texW * texH, Pixel);
		memset(blackBuffer, 0, texW * texH * sizeof(Pixel));
		for(int y=0; y<bufH; ++y)
		{
			Pixel* dst = blackBuffer + y*texW;
			const Pixel* src = mBuffer + y*bufW;
			for(int x=0; x<bufW; ++x) dst[x] = src[x];
		}
		gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, texW, texH, GL_RGBA, GL_UNSIGNED_BYTE, blackBuffer);
		FREE64(blackBuffer);
	}
	else
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texW, texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bufW, bufH, GL_RGBA, GL_UNSIGNED_BYTE, mBuffer);
	}
	if(destroyPixels) DestroyPixels();
	glDisable(GL_TEXTURE_2D);

	return true;
}

void Image::BuildTex(unsigned int externalTexId, int w, int h, double u0, double v0, double u1, double v1)
{
	DestroyPixels();
	DestroyTexture();

	myHasExternalTexId = true;
	mOpenGlTexId = externalTexId;

	mUVs.u0 = u0;
	mUVs.v0 = v0;
	mUVs.u1 = u1;
	mUVs.v1 = v1;

	mSize.x = w;
	mSize.y = h;
}

void Image::BindTexture()
{
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);
}

void Image::Draw(int x, int y, Pixel color) const
{
	if(!mOpenGlTexId) return;
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);
	RenderTexQuad(x - mOrigin.x, y - mOrigin.y, mSize.x, mSize.y, mUVs, color);
	glDisable(GL_TEXTURE_2D);
}

void Image::Draw(float x, float y, float w, float h, float rotation, Pixel color) const
{
	if(!mOpenGlTexId) return;
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);
	const float originX = (float)mOrigin.x * w / (float)mSize.x;
	const float originY = (float)mOrigin.y * h / (float)mSize.y;
	RenderTexQuad(x, y, w, h, rotation, Vec2f(originX, originY), mUVs, color);
	glDisable(GL_TEXTURE_2D);
}

// ===========================================================
// Tileset class
// ===========================================================

//Tileset::Tileset(const char* imageFile, bool keepPixels, bool buildTex, int texFlags) : 
//	Image(imageFile, keepPixels, buildTex, texFlags)
//{
//}
//
//Tileset::Tileset(const Pixel* bufferRGBA, int w, int h, bool keepPixels, bool buildTex, int texFlags) :
//	Image(bufferRGBA, w, h, keepPixels, buildTex, texFlags)
//{
//}
//
//void Tileset::SetTileSize(int tileW, int tileH, int spacingW, int spacingH)
//{
//	mTileSize.Set(tileW, tileH);
//	mTileSpacing.Set(spacingW, spacingH);
//	mSize = mTileSize;
//}
//
//void Tileset::SetTileIndex(int indexHor, int indexVer) const
//{
//	mTileIdx.Set(indexHor, indexVer);
//}
//
//void Tileset::DrawTile(int x, int y) const
//{
//	if(mOpenGlTexId == -1) return;
//	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);
//	UvRect uvs = GetTileUvs(mTileIdx, mTileSize, mTileSpacing, mImageSize, mUScale, mVScale);
//	RenderTexQuad(x-mOrigin.x, y-mOrigin.y, mSize.x, mSize.y, uvs);
//}
//
//void Tileset::DrawTile(float x, float y, float w, float h, float rotation) const
//{
//	if(mOpenGlTexId == -1) return;
//	glBindTexture(GL_TEXTURE_2D, mOpenGlTexId);
//	const float originX = (float)mOrigin.x*w / (float)mTileSize.x;
//	const float originY = (float)mOrigin.y*h / (float)mTileSize.y;
//	UvRect uvs = GetTileUvs(mTileIdx, mTileSize, mTileSpacing, mImageSize, mUScale, mVScale);
//	RenderTexQuadExt(x, y, w, h, rotation, Vec2f(originX, originY), uvs);
//}

}; // namespace Tmpl
